#!/bin/sh -efu
#
# Copyright (C) 2007-2018  Alexey Gladkov <legion@altlinux.org>
# Copyright (C) 2008-2016  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2008  Alexey I. Froloff <raorn@altlinux.org>
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Rules:
# directive: <from-branch> <to-branch> args
#
# directive  := ( merge | gendiff )
# args       := ( strategy | squash | message | unified | name )
# name       := <file>
# message    := <text>
# unified    := <number>
# strategy   := <git merge strategy>
# squash     := NONE
#
# <git merge strategy> - Use the given merge strategy. See man git-merge(1).
#

. gear-utils-sh-functions

show_help() {
	cat <<EOF
Usage: $PROG [Options]

This utility might help you to store upstream sources, patches
and metafiles to facilitate packaging for different repository branches.

Its main purpose is merging differerent branches (usually upstream
sources and/or patch branches) into single master branch.

Options:

  -I, --no-interactive    merge branches non-interactively;
  -a, --add               add new patches in git repository;
  -r, --rules=FILENAME    name of file with rules, default is .gear/merge;
  -s, --stop=BRANCH       stop merge when BRANCH shows in <to-branch>,
                          implies --no-interactive;
      --ignore-changed    do not bail if index changed;
      --ignore-untracked  do not bail if untracked or modified files found;
  -v, --verbose           print a message for each action;
  -V, --version           print program version and exit;
  -h, --help              show this text and exit.

Report bugs to http://bugzilla.altlinux.org/

EOF
	exit
}

print_version() {
	cat <<EOF
$PROG version $PROG_VERSION
Written by Alexey Gladkov <legion@altlinux.org>

Copyright (C) 2007-2018  Alexey Gladkov <legion@altlinux.org>
Copyright (C) 2008-2016  Dmitry V. Levin <ldv@altlinux.org>
Copyright (C) 2008  Alexey I. Froloff <raorn@altlinux.org>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
	exit
}

cur_branch=
set_current_branch() {
	local id="${1:-HEAD}"
	cur_branch="$(git symbolic-ref "$id")"

	[ -z "${cur_branch%%refs/heads/*}" ] ||
		fatal "$id: not a branch."

	cur_branch="${cur_branch#refs/heads/}"
}

restore_branch=
cleanup_handler()
{
	cleanup_workdir
	if [ "$*" = 0 ]; then
		set_current_branch
		[ "$cur_branch" = "$restore_branch" ] ||
			git checkout "$restore_branch"
	fi
}

main_tree_id='HEAD'
rules=
find_merge_rules() {
	: >"$workdir/rules"
	if [ -n "$(git ls-tree "$main_tree_id" "$rules")" ]; then
		cat_blob "$main_tree_id" "$rules" >"$workdir/rules"
	fi
	rules_cleanup "$workdir/rules"
}

validate_rule() {
	local cmd
	cmd="$1"; shift

	for b in "$src_branch" "$dst_branch"; do
		[ -n "$(git ls-remote -h . "refs/heads/$b")" ] ||
			fatal "$b: branch not found."
	done

	[ "$cmd" != 'gendiff' ] || [ -n "$name" ] ||
		rules_error "patch name not defined. Use 'name' option."
}

parse_rule() {
	cmd="${1%:}"; shift
	src_branch='' dst_branch='' name='' message='' squash='' strategy='' unified=''

	case "$cmd" in
		merge|gendiff) ;;
		*) rules_error "unknown command: $cmd" ;;
	esac

	[ "$#" -ge 2 ] ||
		rules_error 'expected syntax: <command:> <from-branch> <to-branch> [<options>]'

	src_branch="$1"; shift
	dst_branch="$1"; shift

	set_current_branch

	while [ "$#" -gt 0 ]; do
		case "$1" in
			name=*)     name="${1#name=}" ;;
			message=*)  message="${1#message=}" ;;
			unified=*)  unified="--unified=${1#unified=}" ;;
			strategy=*) strategy="--strategy=${1#strategy=}" ;;
			squash)     squash='--sq' ;;
			*)          rules_error "unknown option: $1" ;;
		esac
		shift
	done
}

interactive_parse_rules() {
	local ans args app_lineno cur_lineno max_lineno line lineno

	cur_lineno=1
	app_lineno=0

	max_lineno=$(wc -l < "$workdir/rules")
	max_lineno=$(($max_lineno+1))

	ans=
	while :; do
		lineno=0
		while read -r line; do
			lineno=$(($lineno+1))

			[ $lineno -ge $cur_lineno ] ||
				continue

			if [ -z "${line%%#*}" ]; then
				cur_lineno=$(($cur_lineno+1))
				app_lineno=0
				continue
			fi

			quote_shell_args args "$line"
			eval "set -- $args"
			parse_rule "$@"

			if [ $lineno -eq $app_lineno ]; then
				branch_$cmd

				cur_lineno=$(($cur_lineno+1))
				continue
			fi
			break
		done < "$workdir/rules"

		[ $max_lineno != $cur_lineno ] ||
			break

		if [ "$ans" != "all" ]; then
			printf '======\n%s' "Confirm $cmd '$src_branch' -> '$dst_branch' (rule $lineno) [Y/n/a/q]: "
			read -r ans
		fi

		case "$ans" in
			[nN]*)
				cur_lineno=$(($lineno+1))
				app_lineno=0
				;;
			[aA]*)
				cur_lineno=$lineno
				app_lineno=$lineno
				ans="all"
				;;
			[qQ]*)
				break
				;;
			*)
				cur_lineno=$lineno
				app_lineno=$lineno
				;;
		esac
	done
}

stop=
parse_rules() {
	local parser_func="${1-}"
	local line cmd lineno=0

	echo >>"$workdir/rules"
	while read -r line; do
		lineno=$(($lineno+1))

		[ -n "${line%%#*}" ] ||
			continue

		quote_shell_args args "$line"
		eval "set -- $args"
		parse_rule "$@"

		if [ -n "$parser_func" ]; then
			$parser_func "$cmd"
			continue
		elif [ -n "$stop" ] && [ "$dst_branch" = "$stop" ]; then
			 break
		fi

		branch_$cmd

	done < "$workdir/rules"
}

is_already_merged() {
	[ -z "$(git rev-list --max-count=1 "$1".."$2")" ] || return 1
}

branch_merge() {
	if is_already_merged "$dst_branch" "$src_branch"; then
		verbose "No new commits in branch '$dst_branch'"
		return 0
	fi

	[ "$cur_branch" = "$dst_branch" ] ||
		git checkout "$dst_branch"

	git merge $squash $strategy \
		${message:+-m "$message"} \
		"refs/heads/$src_branch" ||
		fatal "Unable to merge '$src_branch' automatically"
}

new_files=
add_new_files=
branch_gendiff() {
	branch_merge

	local dir="${name%/*}"
	[ "$dir" = "$name" ] || [ -d "$dir" ] ||
		mkdir -p -- "$dir"

	{ [ -z "$message" ] || printf '%s\n\n' "$message"; } > "$name"

	git diff $unified \
		-p \
		--no-color \
		--ignore-space-change \
		"refs/heads/$src_branch..refs/heads/$dst_branch" >> "$name" ||
		fatal "Unable to generate diff between '$src_branch' and '$dst_branch'"

	[ -z "$add_new_files" ] ||
		new_files="$new_files $name"
}

TEMP=`getopt -n $PROG -o a,r:,s:,I,V,v,h -l add,rules:,stop:,no-interactive,ignore-changed,ignore-untracked,version,verbose,help -- "$@"` ||
	show_usage
eval set -- "$TEMP"

no_interactive=
rules='.gear/merge'
skip_changed=
skip_untracked=
while :; do
	case "$1" in
		-I|--no-interactive) no_interactive=1
			;;
		-a|--add) add_new_files=1
			;;
		-r|--rules) shift; rules="$1"
			;;
		-s|--stop) shift; stop="$1"; no_interactive=1
			;;
		--ignore-changed) skip_changed=1
			;;
		--ignore-untracked) skip_untracked=1
			;;
		-v|--verbose) verbose=-v
			;;
		-V|--version) print_version
			;;
		-h|--help) show_help
			;;
		--) shift; break
			;;
		*) fatal "Unrecognized option: $1"
			;;
	esac
	shift
done

# Save git directory for future use.
git_dir="$(git rev-parse --git-dir)"
git_dir="$(readlink -ev -- "$git_dir")"

# Change to toplevel directory.
chdir_to_toplevel

if [ -z "$skip_changed" ]; then
	out="$(git diff-index --cached --name-only HEAD)"
	[ -z "$out" ] ||
		fatal "Changed files found in the index."
fi

if [ -z "$skip_untracked" ]; then
	out="$(git diff --name-only &&
	       git ls-files --directory --others --exclude-per-directory=.gitignore)"
	[ -z "$out" ] ||
		fatal "Untracked or modified files found."
fi

GEAR_MERGE=1
export GEAR_MERGE

set_current_branch
restore_branch="$cur_branch"

install_cleanup_handler cleanup_handler
workdir="$(mktemp -dt "$PROG.XXXXXXXX")"

find_merge_rules

if [ ! -s "$workdir/rules" ]; then
	message "No rules found."
	show_usage
	exit 0
fi

parse_rules 'validate_rule'

if [ -z "$no_interactive" ]; then
	interactive_parse_rules
else
	parse_rules
fi

if [ -n "$add_new_files" ] && [ -n "$new_files" ]; then
	set_current_branch
	if [ "$cur_branch" != "$restore_branch" ]; then
		git checkout "$restore_branch"
		restore_branch=
	fi
	git add $new_files
fi
